package ru.compscicenter.uno.view;

import ru.compscicenter.uno.model.Card;
import ru.compscicenter.uno.model.IModel;
import ru.compscicenter.uno.model.ModelException;
import ru.compscicenter.uno.model.Player;
import ru.compscicenter.uno.util.ImagePool;
import ru.compscicenter.uno.view.animation.ActionAnimation;
import ru.compscicenter.uno.view.animation.SimpleCardTransferAnimation;
import ru.compscicenter.uno.view.animation.TurnSwitchAnimation;
import ru.compscicenter.uno.view.events.DropCardListener;
import ru.compscicenter.uno.view.events.TakeCardEvent;
import ru.compscicenter.uno.view.events.TakeCardListener;

import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.Timer;
import javax.swing.event.EventListenerList;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.*;

/**
 * Created by IntelliJ IDEA.
 * User: Павел
 * Date: 26.11.11
 * Time: 1:40
 * To change this template use File | Settings | File Templates.
 */
public class GamePanel extends JPanel {
    private IModel model;

    private CardImage top;
    private CardImage deck;
    private final ArrayList<OtherPlayerPanel> otherPlayers = new ArrayList<>();
    private MyPlayerPanel myPlayer;

    private ImageButton endTurnButton;
    private ImageButton startNewRoundButton;
    private ImageButton startNewGameButton;
    private JLabel roundResults;

    private BufferedImage backgroundImage;
    private static final String backgroundImagePath = "res/img/background.png";

    private BufferedImage turnSwitchIndicator;
    private static final String turnSwitchIndicatorImagePath = "res/img/players/turn_switch_indicator.png";
    private Point turnSwitchIndicatorCurrentPoint;
    private Point turnSwitchIndicatorStartPoint;
    private Point turnSwitchIndicatorEndPoint;

    private BufferedImage leftTurnDirectionArrow;
    private BufferedImage rightTurnDirectionArrow;

    private static final int ARROWS_PADDING = 10;
    private static final int TURN_SWITCH_INDICATOR_SIZE = 30;

    private BufferedImage actionImage;
    private Rectangle actionAnimationPosition;
    private float actionImageOpacity = 0;
    private final boolean isServer;

    private BufferedImage loadImage(String path) {
        try {
            return ImageIO.read(new File(path));
        } catch (IOException e) {
            System.err.println("error: " + e.getMessage() + " " + e);
            return null;
        }
    }

    public GamePanel(final IModel model, boolean isServer) {
        this.model = model;
        this.isServer = isServer;
        setLayout(null);
        addComponentListener(new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent e) {
                backgroundImage = ImagePool.getImage(backgroundImagePath, getWidth(), getHeight());
                replaceComponents();
                updateTurnDirectionArrows();
                repaint();
            }
        });

        setBackground(new Color(0x009900));

        top = new CardImage(model.getTopCard());

        deck = new CardImage(null);
        deck.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseReleased(MouseEvent e) {
                fireTakeCardEvent(new TakeCardEvent(this));
            }
        });
        add(top);
        add(deck);

        endTurnButton = new ImageButton("end_turn");
        add(endTurnButton);

        startNewRoundButton = new ImageButton("new_round");
        startNewGameButton = new ImageButton("new_game");
        roundResults = new JLabel();
        Font nameFont = new Font("Arial", Font.BOLD, 14);
        roundResults.setForeground(Color.white);
        roundResults.setFont(nameFont);
        add(roundResults);
        add(startNewGameButton);
        add(startNewRoundButton);

        int myId = model.getMyPlayerId();
        for (Player p : model.getPlayers()) {
            if (p.getId() != myId) {
                addOtherPlayer(p);
            }
        }

        myPlayer = new MyPlayerPanel(model.getPlayer(model.getMyPlayerId()));
        ImageButton sayUno = new ImageButton("say_uno");
        sayUno.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseReleased(MouseEvent e) {
                try {
                    model.doSayUno(model.getMyPlayerId());
                } catch (ModelException e1) {
                    //TODO
                }
            }
        });
        ImageButton alreadySayUno = new ImageButton("already_say_uno");
        myPlayer.setSayUno(sayUno);
        myPlayer.setAlreadySayUno(alreadySayUno);
        add(myPlayer);

        setBackground(Color.BLACK);
        setOpaque(false);
        turnSwitchIndicator = ImagePool.getImage(turnSwitchIndicatorImagePath, TURN_SWITCH_INDICATOR_SIZE, TURN_SWITCH_INDICATOR_SIZE);

        updateTurnDirectionArrows();
        update();

        addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                if (e.getKeyCode() == KeyEvent.VK_SPACE) {
                    try {
                        model.doSayUno(model.getMyPlayerId());
                    } catch (ModelException e1) {
                        //TODO
                    }
                }
            }
        });
    }

    public void setModel(IModel model) {
        this.model = model;
    }

    public void update() {
        System.out.println("UPDATING VIEW");
        for (OtherPlayerPanel otherPlayer : otherPlayers) {
            int id = otherPlayer.getPlayer().getId();
            otherPlayer.setPlayer(model.getPlayer(id));
            otherPlayer.update(otherPlayer.getPlayer().getId() == model.getCurrentPlayerId());
        }

        myPlayer.setPlayer(model.getPlayer(model.getMyPlayerId()));
        boolean isMyTurn = model.getMyPlayerId() == model.getCurrentPlayerId();
        myPlayer.update(isMyTurn);

        endTurnButton.setVisible(model.isIDidSomething());

        //обновляем последнюю сыгранную карту
        remove(top);
        Card topCard = model.getTopCard();
        if (topCard.isWild()) {
            //если это специальная карта, то создадим "раскрашенную" wild-карту
            top = new CardImage(topCard, model.getTopColor());
        } else {
            top = new CardImage(topCard);
        }
        add(top);

        if (model.isRoundEnded() || model.isGameEnded()) {
            if (isServer) {
                if (model.isGameEnded()) {
                    startNewRoundButton.setVisible(false);
                    startNewGameButton.setVisible(true);
                } else {
                    startNewRoundButton.setVisible(true);
                    startNewGameButton.setVisible(false);
                }
            }
            endTurnButton.setVisible(false);
            roundResults.setText(createResultsMessage());
            roundResults.setVisible(true);
            top.setVisible(false);
            deck.setVisible(false);
        } else {
            startNewGameButton.setVisible(false);
            startNewRoundButton.setVisible(false);
            roundResults.setVisible(false);
            top.setVisible(true);
            deck.setVisible(true);
        }

        updateTurnDirectionArrows();
        replaceCenterCards();

        this.repaint();
    }

    private String createResultsMessage() {
        StringBuffer str = new StringBuffer();

        java.util.List players = model.getPlayers();
        for (Object o : players) {
            Player player = (Player) o;
            str.append(player.getName() + ": " + player.getPoints() + "\n");
        }

        Player winner = model.getPlayer(model.getRoundWinnerPlayerId());
        return convertToMultiline("Победитель: " + winner.getName() + "." +
                "\n\nОчки:" +
                "\n" + str.toString());
    }

    public static String convertToMultiline(String orig) {
        //swing JLabel supports html, but doesn't support '\n' o_O
        return "<html>" + orig.replaceAll("\n", "<br>");
    }

    public ImageButton getStartNewRoundButton() {
        return startNewRoundButton;
    }

    public ImageButton getStartNewGameButton() {
        return startNewGameButton;
    }

    private void updateTurnDirectionArrows() {
        int arrowsHeight = top.getHeight();
        // proportion = 340/150 = 2.26
        int arrowsWidth = Math.round(arrowsHeight / 2.26f);

        String leftArrowPath = "res/img/arrows/arrow_left_" + (model.isClockwise() ? "down" : "up") + ".png";
        String rightArrowPath = "res/img/arrows/arrow_right_" + (model.isClockwise() ? "up" : "down") + ".png";
        leftTurnDirectionArrow = ImagePool.getImage(leftArrowPath, arrowsWidth, arrowsHeight);
        rightTurnDirectionArrow = ImagePool.getImage(rightArrowPath, arrowsWidth, arrowsHeight);
    }

    private void addOtherPlayer(final Player p) {
        OtherPlayerPanel playerPanel = new OtherPlayerPanel(p);
        ImageButton didNotSayUno = new ImageButton("did_not_say_uno");
        didNotSayUno.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseReleased(MouseEvent e) {
                try {
                    model.doSomebodyDidntSayUno(model.getMyPlayerId(), p.getId());
                } catch (ModelException e1) {
                    //TODO
                }
            }
        });
        ImageButton alreadySayUno = new ImageButton("already_say_uno");
        playerPanel.setAlreadySayUno(alreadySayUno);
        playerPanel.setDidNotSayUno(didNotSayUno);
        otherPlayers.add(getPositionToInsertOtherPlayer(p), playerPanel);

        add(playerPanel);
        replaceOtherPlayers();
    }

    private int getPositionToInsertOtherPlayer(Player player) {
        int position = 0;
        int m = model.getPlayersCount() - 1;
        int myId = model.getMyPlayerId();
        int playerId = player.getId();

        if (playerId > myId) {
            for (position = 0; position < otherPlayers.size(); position++) {
                OtherPlayerPanel otherPlayerPanel = otherPlayers.get(position);
                Player otherPlayer = otherPlayerPanel.getPlayer();
                if (playerId < otherPlayer.getId() || otherPlayer.getId() == 0) {
                    break;
                }
            }
        } else {
            for (position = otherPlayers.size() - 1; position >= 0; position--) {
                OtherPlayerPanel otherPlayerPanel = otherPlayers.get(position);
                Player otherPlayer = otherPlayerPanel.getPlayer();
                if (playerId > otherPlayer.getId() || otherPlayer.getId() == m) {
                    position++;
                    break;
                }
            }
        }
        if (position < 0) {
            position = 0;
        }
        return position;
    }

    private void replaceComponents() {
        replaceCenterCards();
        replaceMyPlayer();
        replaceOtherPlayers();
    }

    private void replaceCenterCards() {
        int centerCardWidth = getWidth() / 10;
        int centerCardHeight = CardImage.getProportionHeight(centerCardWidth);
        int maxCenterCardHeight = getHeight() / 4;
        if (centerCardHeight > maxCenterCardHeight) {
            centerCardHeight = maxCenterCardHeight;
            centerCardWidth = CardImage.getProportionWidth(maxCenterCardHeight);
        }
        int topCardX = (getWidth() - 2 * centerCardWidth - 20) / 2;
        int deckCardX = (getWidth() - centerCardWidth - topCardX);

        int centerCardsY = (getHeight() - centerCardHeight) / 2;

        top.setBounds(topCardX, centerCardsY, centerCardWidth, centerCardHeight);
        deck.setBounds(deckCardX, centerCardsY, centerCardWidth, centerCardHeight);

        int buttonPadding = 10;
        int buttonWidth = deck.getX() + deck.getWidth() - (top.getX());
        int buttonHeight = Math.round(buttonWidth / endTurnButton.getProportionOfOriginalImage());
        endTurnButton.setBounds(topCardX, centerCardsY - buttonPadding - buttonHeight,
                buttonWidth, buttonHeight);

        roundResults.setBounds(topCardX, centerCardsY, deckCardX + centerCardWidth - topCardX, centerCardHeight);
        startNewGameButton.setBounds(topCardX, centerCardsY - buttonPadding - buttonHeight,
                buttonWidth, buttonHeight);
        startNewRoundButton.setBounds(topCardX, centerCardsY - buttonPadding - buttonHeight,
                buttonWidth, buttonHeight);
    }

    private void replaceMyPlayer() {
        int h = getHeight() / 3;
        int y = getHeight() - h;
        myPlayer.setBounds(0, y, getWidth(), h);
    }

    private void replaceOtherPlayers() {
        int scaledPlayerPanelWidth = getWidth() / otherPlayers.size() * 6 / 5;
        int scaledPlayerPanelHeight = getHeight() / 4;

        int xOutputPadding = scaledPlayerPanelWidth / 3 + 10;
        int yOutputPadding = scaledPlayerPanelHeight / 3 + 30;
        int fullW = getWidth() - xOutputPadding * 2;
        int fullH = getHeight() - yOutputPadding;
        int b = fullW / 2;
        int a = fullH / 2;
        int r = a;
        double k = (double) b / (double) a; //коэфф. растяжения эллипса в ширину
        double step = Math.PI / otherPlayers.size();
        double halfStep = step / 2;

        for (int i = 0; i < otherPlayers.size(); i++) {
            OtherPlayerPanel player = otherPlayers.get(i);
            int x = (int) Math.round((b + r * Math.cos(i * step + halfStep) * k)) + xOutputPadding;
            int y = (int) Math.round(a - r * Math.sin(i * step + halfStep)) + yOutputPadding;

            player.setBounds(x - scaledPlayerPanelWidth / 2, y - scaledPlayerPanelHeight / 2,
                    scaledPlayerPanelWidth, scaledPlayerPanelHeight);
        }
    }

    public ImageButton getEndTurnButton() {
        return endTurnButton;
    }

    protected EventListenerList listenerList = new EventListenerList();

    public void addTakeCardEventListener(TakeCardListener listener) {
        listenerList.add(TakeCardListener.class, listener);
    }

    public void removeTakeCardEventListener(TakeCardListener listener) {
        listenerList.remove(TakeCardListener.class, listener);
    }

    void fireTakeCardEvent(TakeCardEvent evt) {
        Object[] listeners = listenerList.getListenerList();
        for (int i = 0; i < listeners.length; i = i + 2) {
            if (listeners[i] == TakeCardListener.class) {
                ((TakeCardListener) listeners[i + 1]).takeCardEventOccurred(evt);
            }
        }
    }

    public void addDropCardEventListener(DropCardListener listener) {
        myPlayer.addDropCardEventListener(listener);
    }

    public void removeDropCardEventListener(DropCardListener listener) {
        myPlayer.removeDropCardEventListener(listener);
    }

    public void animateTakeCardMyPlayer() {
        Rectangle finishPos = new Rectangle(myPlayer.getX() + myPlayer.getLastCardX(), myPlayer.getY() + myPlayer.getCardYLevel(), myPlayer.getOneCardWidth(), myPlayer.getOneCardHeight());
        animateTakeCard(finishPos);
    }

    public void animateTakeCardOtherPlayer(int playerId) {
        Rectangle finishPos = null;
        for (OtherPlayerPanel otherPlayer : otherPlayers) {
            if (otherPlayer.getPlayer().getId() == playerId) {
                CardImage cardImage = otherPlayer.getFirstCard();
                if (cardImage != null) {
                    finishPos = new Rectangle(otherPlayer.getX() + cardImage.getX(), otherPlayer.getY() + cardImage.getY(), cardImage.getWidth(), cardImage.getHeight());
                }
                break;
            }
        }
        if (finishPos != null) {
            animateTakeCard(finishPos);
        }   //todo if else. it is impossible
    }

    public void animateTakeCard(Rectangle finishPosition) {
        final CardImage card = new CardImage(null);
        card.setBounds(deck.getX(), deck.getY(), deck.getWidth(), deck.getHeight());
        card.setHighRender(false);
        add(card);

        final int ANIMATION_TIME = 300;
        final int DELAY = 50;
        final SimpleCardTransferAnimation animation = new SimpleCardTransferAnimation(deck.getX(), deck.getY(), deck.getWidth(), deck.getHeight(),
                finishPosition.x, finishPosition.y, finishPosition.width, finishPosition.height,
                System.currentTimeMillis(), System.currentTimeMillis() + ANIMATION_TIME);

        final Timer timer = new Timer(DELAY, null);
        timer.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                final long startTime = animation.getStartTime();
                final int time = (int) (System.currentTimeMillis() - startTime);
                card.setBounds(
                        animation.getX(time),
                        animation.getY(time),
                        animation.getW(time),
                        animation.getH(time)
                );

                if (animation.isFinished(time)) {
                    timer.stop();
                    remove(card);
                }
                GamePanel.this.repaint();
            }
        });
        timer.start();
    }

    public void animateDropCardOtherPlayer(int playerId, int dropingCardId) {
        //model has been already changed to this moment, now dropping card is top
        final Card card = model.getTopCard();
        Rectangle startPosition = null;
        for (OtherPlayerPanel otherPlayer : otherPlayers) {
            if (otherPlayer.getPlayer().getId() == playerId) {
                CardImage cardImage = otherPlayer.getFirstCard();
                if (cardImage != null) {
                    startPosition = new Rectangle(otherPlayer.getX() + cardImage.getX(), otherPlayer.getY() + cardImage.getY(), cardImage.getWidth(), cardImage.getHeight());
                }  //todo if else
                break;
            }
        }

        if (startPosition != null && card != null) {
            animateDropCard(card, startPosition);
        }    //todo if else. it is impossible
    }

    public void animateDropCardMyPlayer(CardImage cardImage) {
        Rectangle startPosition = new Rectangle(myPlayer.getX() + cardImage.getX(), myPlayer.getY() + cardImage.getY(), cardImage.getWidth(), cardImage.getHeight());
        Card card = cardImage.getCard();
        animateDropCard(card, startPosition);
    }

    public void animateDropCard(Card droppingCard, Rectangle startPosition) {
        final CardImage card = new CardImage(droppingCard);
        card.setBounds(startPosition);
        card.setHighRender(false);
        add(card);

        final int ANIMATION_TIME = 300;
        final int DELAY = 50;

        final SimpleCardTransferAnimation animation = new SimpleCardTransferAnimation(
                startPosition.x, startPosition.y, startPosition.width, startPosition.height,
                top.getX(), top.getY(), top.getWidth(), top.getHeight(),
                System.currentTimeMillis(), System.currentTimeMillis() + ANIMATION_TIME);

        final Timer timer = new Timer(DELAY, null);
        timer.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                final long startTime = animation.getStartTime();
                final int time = (int) (System.currentTimeMillis() - startTime);
                card.setBounds(
                        animation.getX(time),
                        animation.getY(time),
                        animation.getW(time),
                        animation.getH(time)
                );

                if (animation.isFinished(time)) {
                    timer.stop();
                    remove(card);
                }
                GamePanel.this.repaint();
            }
        });
        timer.start();
    }

    public void animateTurnSwitch(int playerId) {
        if (turnSwitchIndicatorCurrentPoint != null) {
            return; //not supporting to multiple animations
        }

        if (model.getCurrentPlayerId() == playerId) {
            return; //turn didn't changed (for example, 2 players and one drop Skip card)
        }

        if (playerId == model.getMyPlayerId()) {
            turnSwitchIndicatorStartPoint = myPlayer.getPlayerPanelLocation();
        } else {
            for (OtherPlayerPanel otherPlayer : otherPlayers) {
                if (otherPlayer.getPlayer().getId() == playerId) {
                    turnSwitchIndicatorStartPoint = otherPlayer.getPlayerPanelLocation();
                }
            }
        }

        if (model.getCurrentPlayerId() == model.getMyPlayerId()) {
            turnSwitchIndicatorEndPoint = myPlayer.getPlayerPanelLocation();
        } else {
            for (OtherPlayerPanel otherPlayer : otherPlayers) {
                if (otherPlayer.getPlayer().getId() == model.getCurrentPlayerId()) {
                    turnSwitchIndicatorEndPoint = otherPlayer.getPlayerPanelLocation();
                }
            }
        }

        turnSwitchIndicatorCurrentPoint = turnSwitchIndicatorStartPoint;

        final int ANIMATION_TIME = 700;
        final int DELAY = 25;

        final TurnSwitchAnimation animation = new TurnSwitchAnimation(
                turnSwitchIndicatorStartPoint.x, turnSwitchIndicatorStartPoint.y,
                turnSwitchIndicatorEndPoint.x, turnSwitchIndicatorEndPoint.y,
                System.currentTimeMillis(), System.currentTimeMillis() + ANIMATION_TIME);

        final Timer timer = new Timer(DELAY, null);
        timer.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                final long startTime = animation.getStartTime();
                final int time = (int) (System.currentTimeMillis() - startTime);
                turnSwitchIndicatorCurrentPoint.setLocation(animation.getX(time), animation.getY(time));

                if (animation.isFinished(time)) {
                    timer.stop();
                    turnSwitchIndicatorCurrentPoint = null;
                }
                GamePanel.this.repaint();
            }
        });
        timer.start();
    }

    private Rectangle getActionAnimationBounds(int playerId) {
        if (playerId == model.getMyPlayerId()) {
            int width = 400;
            int height = 350;
            return new Rectangle((getWidth() - width) / 2, (getHeight() - height) / 2, width, height);
        } else {
            for (OtherPlayerPanel otherPlayer : otherPlayers) {
                if (otherPlayer.getPlayer().getId() == playerId) {
                    Point point = otherPlayer.getPlayerPanelLocation();
                    int width = 200;
                    int height = 120;
                    return new Rectangle(point.x - width / 2, point.y - height, width, height);
                }
            }
        }
        return null;
    }

    private void animateAction(String path) {
        animateAction(path, model.getMyPlayerId());
    }

    private void animateAction(String path, int playerId) {
        if (actionImageOpacity > 0) {
            return; //todo: resolve better
        }
        actionImage = loadImage("res/img/actions/action_" + path + ".png");
        actionAnimationPosition = getActionAnimationBounds(playerId);

        final int ANIMATION_TIME = 1600;
        final int DELAY = 200;

        final ActionAnimation animation = new ActionAnimation(System.currentTimeMillis(), System.currentTimeMillis() + ANIMATION_TIME);

        final Timer timer = new Timer(DELAY, null);
        timer.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                final long startTime = animation.getStartTime();
                final int time = (int) (System.currentTimeMillis() - startTime);

                actionImageOpacity = animation.getOpacity(time);

                if (animation.isFinished(time)) {
                    timer.stop();
                    actionImageOpacity = 0;
                }
                GamePanel.this.repaint();
            }
        });
        timer.start();
    }

    public void animateSayUno(int playerId) {
        animateAction("uno", playerId);
    }

    public void animateDidNotSayUno(int whoDidNotSayId) {
        animateAction("did_not_say_uno", whoDidNotSayId);
    }

    public void animatePlus2(int playerId) {
        animateAction("plus2", model.nextPlayerId(playerId));
    }

    public void animatePlus4(int playerId) {
        animateAction("plus4", model.nextPlayerId(playerId));
    }

    public void animateSkip(int playerId) {
        animateAction("skip", model.nextPlayerId(playerId));
    }

    public void animateReverse() {
        animateAction("reverse");
    }

    public void animateColorChanged() {
        animateAction("color_changed");
    }


    @Override
    public void paint(Graphics g) {
        g.drawImage(backgroundImage, 0, 0, getWidth(), getHeight(), null);

        if (!(model.isRoundEnded() || model.isGameEnded())) {
            g.drawImage(leftTurnDirectionArrow, top.getX() - leftTurnDirectionArrow.getWidth() - ARROWS_PADDING, top.getY(),
                    leftTurnDirectionArrow.getWidth(), leftTurnDirectionArrow.getHeight(), null);
            g.drawImage(rightTurnDirectionArrow, deck.getX() + deck.getWidth() + ARROWS_PADDING, deck.getY(),
                    rightTurnDirectionArrow.getWidth(), rightTurnDirectionArrow.getHeight(), null);
        }

        super.paintChildren(g);

        if (turnSwitchIndicatorCurrentPoint != null) {
            g.drawImage(turnSwitchIndicator, turnSwitchIndicatorCurrentPoint.x, turnSwitchIndicatorCurrentPoint.y,
                    turnSwitchIndicator.getWidth(), turnSwitchIndicator.getHeight(), null);
        }

        if (actionImageOpacity > 0) {
            Graphics2D g2d = (Graphics2D) g;

            g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
                    actionImageOpacity));

            g2d.drawImage(actionImage, actionAnimationPosition.x, actionAnimationPosition.y, actionAnimationPosition.width, actionAnimationPosition.height, null);
        }
    }
}
